﻿namespace UnitFundProfitability.BusinessLogic
{
	using System;
	using System.Collections.Generic;
	using System.Collections.ObjectModel;
	using System.Configuration;
	using System.Globalization;
	using System.Linq;

	/// <summary>
	/// Class which contains all operations, related to unit funds.
	/// </summary>
	public static class Manager
	{
		#region Constants
		private static readonly decimal TaxRate = decimal.Parse(ConfigurationManager.AppSettings["TaxRate"], CultureInfo.InvariantCulture);
		#endregion

		#region Methods
		/// <summary>
		/// Adds the expenditure.
		/// </summary>
		/// <param name="expenditure">The expenditure.</param>
		/// <param name="expenditureDate">The expenditure date.</param>
		public static void AddExpenditure(Expenditure expenditure, DateTime expenditureDate)
		{
			if (expenditure == null)
			{
				throw new ArgumentNullException("expenditure");
			}
			if (expenditure.UnitsAmount == 0 && expenditure.MoneyAmount == 0)
			{
				throw new InvalidOperationException("Units amount or money amount should not be equal to zero.");
			}
			if (expenditure.UnitsAmount != 0 && expenditure.MoneyAmount != 0)
			{
				throw new InvalidOperationException("Units amount or money amount should be equal to zero.");
			}
			if (expenditure.UnitsAmount < 0 || expenditure.MoneyAmount < 0)
			{
				throw new InvalidOperationException("Units amount and money amount could not be less than zero.");
			}
			using (UnitFundsEntities dataContext = new UnitFundsEntities())
			{
				MarkdownRate markdownRate = GetLastMarkdownRate(expenditureDate, dataContext, expenditure.FundId);
				UnitPrice price = GetFirstPrice(expenditureDate, dataContext, expenditure.FundId);
				IEnumerable<Income> activeIncomes = GetActiveIncomes(expenditureDate, dataContext, expenditure.FundId);
				IEnumerator<Income> incomesEnumerator = activeIncomes.GetEnumerator();
				if (expenditure.UnitsAmount != 0)
				{
					WriteOffUnitsAmount(expenditure, expenditureDate, markdownRate, price, incomesEnumerator);
				}
				else
				{
					WriteOffMoneyAmount(expenditure, expenditureDate, markdownRate, price, incomesEnumerator);
				}
				expenditure.ExpenditureId = Guid.NewGuid();
				Operation operation = new Operation();
				operation.OperationId = expenditure.ExpenditureId;
				operation.OperationDate = expenditureDate;
				operation.OperationTypeId = (int)OperationType.Income;
				dataContext.Expenditures.AddObject(expenditure);
				dataContext.Operations.AddObject(operation);
				dataContext.SaveChanges();
			}
		}

		/// <summary>
		/// Adds the income.
		/// </summary>
		/// <param name="income">The income.</param>
		/// <param name="incomeDate">The income date.</param>
		public static void AddIncome(Income income, DateTime incomeDate)
		{
			if (income == null)
			{
				throw new ArgumentNullException("income");
			}
			if (income.UnitsAmount == 0 && income.MoneyAmount == 0)
			{
				throw new InvalidOperationException("Units amount or money amount should not be equal to zero.");
			}
			if (income.UnitsAmount != 0 && income.MoneyAmount != 0)
			{
				throw new InvalidOperationException("Units amount or money amount should be equal to zero.");
			}
			if (income.UnitsAmount < 0 || income.MoneyAmount < 0)
			{
				throw new InvalidOperationException("Units amount and money amount could not be less than zero.");
			}
			using (UnitFundsEntities dataContext = new UnitFundsEntities())
			{
				MarkupRate markupRate = GetLastMarkupRate(incomeDate, dataContext, income.FundId);
				UnitPrice price = GetFirstPrice(incomeDate, dataContext, income.FundId);
				income.IncomeId = Guid.NewGuid();
				Operation operation = new Operation();
				operation.OperationId = income.IncomeId;
				operation.OperationDate = incomeDate;
				operation.OperationTypeId = (int)OperationType.Income;
				if (price != null)
				{
					AdjustIncomeUnitsAmount(income, markupRate, price);
				}
				else
				{
					operation.IsPendingOperation = true;
				}
				dataContext.Incomes.AddObject(income);
				dataContext.Operations.AddObject(operation);
				dataContext.SaveChanges();
			}
		}

		/// <summary>
		/// Adds the management company.
		/// </summary>
		/// <param name="managementCompany">The management company.</param>
		public static void AddManagementCompany(ManagementCompany managementCompany)
		{
			if (managementCompany == null)
			{
				throw new ArgumentNullException("managementCompany");
			}
			using (UnitFundsEntities dataContext = new UnitFundsEntities())
			{
				dataContext.ManagementCompanies.AddObject(managementCompany);
				dataContext.SaveChanges();
			}
		}

		/// <summary>
		/// Adds the movement.
		/// </summary>
		/// <param name="unitsAmount">The units amount.</param>
		/// <param name="sourceFund">The source fund.</param>
		/// <param name="destinationFund">The destination fund.</param>
		/// <param name="movementDate">The movement date.</param>
		public static void AddMovement(decimal unitsAmount, UnitFund sourceFund, UnitFund destinationFund, DateTime movementDate)
		{
			if (sourceFund == null)
			{
				throw new ArgumentNullException("sourceFund");
			}
			if (destinationFund == null)
			{
				throw new ArgumentNullException("destinationFund");
			}
			if (unitsAmount <= 0)
			{
				throw new ArgumentException("Units amount should be greater than zero.", "unitsAmount");
			}
			if (sourceFund.CompanyId != destinationFund.CompanyId)
			{
				throw new InvalidOperationException("Unit funds should be in the same management company.");
			}
			using (UnitFundsEntities dataContext = new UnitFundsEntities())
			{
				UnitPrice sourceUnitPrice = GetFirstPrice(movementDate, dataContext, sourceFund.FundId);
				UnitPrice destinationUnitPrice = GetFirstPrice(movementDate, dataContext, destinationFund.FundId);
				IEnumerable<Income> activeIncomes = GetActiveIncomes(movementDate, dataContext, sourceFund.FundId);
				IEnumerator<Income> incomesEnumerator = activeIncomes.GetEnumerator();
				Operation operation = new Operation();
				operation.OperationId = Guid.NewGuid();
				operation.OperationDate = movementDate;
				operation.OperationTypeId = (int)OperationType.Exchange;
				Expenditure expenditure = new Expenditure();
				expenditure.ExpenditureId = operation.OperationId;
				expenditure.FundId = sourceFund.FundId;
				expenditure.UnitsAmount = unitsAmount;
				expenditure.MoneyAmount = unitsAmount * sourceUnitPrice.Price;
				Income income = new Income();
				income.IncomeId = operation.OperationId;
				income.FundId = destinationFund.FundId;
				WriteOffUnitsAmountForMovement(unitsAmount, sourceUnitPrice, destinationUnitPrice, incomesEnumerator, income);
				dataContext.Operations.AddObject(operation);
				dataContext.Expenditures.AddObject(expenditure);
				dataContext.Incomes.AddObject(income);
				dataContext.SaveChanges();
			}
		}

		/// <summary>
		/// Adds the unit fund.
		/// </summary>
		/// <param name="unitFund">The unit fund.</param>
		public static void AddUnitFund(UnitFund unitFund)
		{
			if (unitFund == null)
			{
				throw new ArgumentNullException("unitFund");
			}
			using (UnitFundsEntities dataContext = new UnitFundsEntities())
			{
				dataContext.UnitFunds.AddObject(unitFund);
				dataContext.SaveChanges();
			}
		}

		/// <summary>
		/// Updates the unit fund.
		/// </summary>
		/// <param name="unitFund">The unit fund.</param>
		public static void UpdateUnitFund(UnitFund unitFund)
		{
			if (unitFund == null)
			{
				throw new ArgumentNullException("unitFund");
			}
			using (UnitFundsEntities dataContext = new UnitFundsEntities())
			{
				UnitFund unitFundFromDatabase =
					(from item in dataContext.UnitFunds
					 where item.FundId == unitFund.FundId
					 select item).Single();
				unitFundFromDatabase.FundName = unitFund.FundName;
				unitFundFromDatabase.MarkdownPeriod = unitFund.MarkdownPeriod;
				dataContext.SaveChanges();
			}
		}

		/// <summary>
		/// Gets the management companies.
		/// </summary>
		/// <returns>List of management companies.</returns>
		public static ReadOnlyCollection<ManagementCompany> GetManagementCompanies()
		{
			List<ManagementCompany> result;
			using (UnitFundsEntities dataContext = new UnitFundsEntities())
			{
				result = dataContext.ManagementCompanies.ToList();
			}
			return new ReadOnlyCollection<ManagementCompany>(result);
		}

		/// <summary>
		/// Gets the unit funds.
		/// </summary>
		/// <returns>List of unit funds.</returns>
		public static ReadOnlyCollection<UnitFund> GetUnitFunds()
		{
			List<UnitFund> result;
			using (UnitFundsEntities dataContext = new UnitFundsEntities())
			{
				result = dataContext.UnitFunds.ToList();
			}
			return new ReadOnlyCollection<UnitFund>(result);
		}

		/// <summary>
		/// Calculates the funds profitabilities.
		/// </summary>
		/// <param name="startDate">The start date.</param>
		/// <param name="endDate">The end date.</param>
		/// <returns>List of funds profitabilities.</returns>
		public static ReadOnlyCollection<FundProfitability> CalculateFundsProfitabilities(DateTime startDate, DateTime endDate)
		{
			if (endDate < startDate)
			{
				throw new ArgumentException("endDate should not be earlier than startDate", "endDate");
			}
			List<FundProfitability> result = new List<FundProfitability>();
			using (UnitFundsEntities dataContext = new UnitFundsEntities())
			{
				ProfitabilityCalculatingHelper calculatingHelper = new ProfitabilityCalculatingHelper();
				calculatingHelper.StartDate = startDate;
				calculatingHelper.EndDate = endDate;
				calculatingHelper.FundsProfitabilites = new Dictionary<Guid, FundProfitability>();
				IEnumerable<UnitFund> unitFunds = GetUnitFunds();
				IQueryable<IGrouping<Guid, MarkdownRate>> startMarkdownRates = GetMarkdownRates(startDate, dataContext);
				IQueryable<IGrouping<Guid, UnitPrice>> startUnitPrices = GetUnitPrices(startDate, dataContext);
				calculatingHelper.StartFundsSnapshots = CreateFundSnapshots(startDate, unitFunds, startUnitPrices, startMarkdownRates);
				IQueryable<IGrouping<Guid, MarkdownRate>> endMarkdownRates = GetMarkdownRates(endDate, dataContext);
				IQueryable<IGrouping<Guid, UnitPrice>> endUnitPrices = GetUnitPrices(endDate, dataContext);
				calculatingHelper.EndFundsSnapshots = CreateFundSnapshots(endDate, unitFunds, endUnitPrices, endMarkdownRates);
				CalculateIncomes(dataContext, calculatingHelper);
				CalculateExpenditures(dataContext, calculatingHelper);
				foreach (Guid unitFund in calculatingHelper.FundsProfitabilites.Keys)
				{
					FundProfitability fundProfitability = calculatingHelper.FundsProfitabilites[unitFund];
					FundSnapshot startFundSnapshot = calculatingHelper.StartFundsSnapshots[unitFund];
					FundSnapshot endFundSnapshot = calculatingHelper.EndFundsSnapshots[unitFund];
					fundProfitability.StartValue = startFundSnapshot.PossibleMoneyExpenditure;
					fundProfitability.EndValue = endFundSnapshot.PossibleMoneyExpenditure;
					result.Add(fundProfitability);
				}
			}
			return new ReadOnlyCollection<FundProfitability>(result);
		}

		/// <summary>
		/// Gets the unit funds by company.
		/// </summary>
		/// <param name="company">The company.</param>
		/// <returns>List of unit funds.</returns>
		public static ReadOnlyCollection<UnitFund> GetUnitFunds(ManagementCompany company)
		{
			if (company == null)
			{
				throw new ArgumentNullException("company");
			}
			List<UnitFund> result;
			using (UnitFundsEntities dataContext = new UnitFundsEntities())
			{
				result =
					(from unitFund in dataContext.UnitFunds
					 where unitFund.CompanyId == company.CompanyId
					 select unitFund).ToList();
			}
			return new ReadOnlyCollection<UnitFund>(result);
		}

		/// <summary>
		/// Sets the markdown rate.
		/// </summary>
		/// <param name="markdownRate">The markdown rate.</param>
		public static void SetMarkdownRate(MarkdownRate markdownRate)
		{
			if (markdownRate == null)
			{
				throw new ArgumentNullException("markdownRate");
			}
			if (markdownRate.Rate < 0)
			{
				throw new ArgumentException("Rate should not be smaller than zero.", "markdownRate");
			}
			using (UnitFundsEntities dataContext = new UnitFundsEntities())
			{
				dataContext.MarkdownRates.AddObject(markdownRate);
				dataContext.SaveChanges();
			}
		}

		/// <summary>
		/// Sets the markup rate.
		/// </summary>
		/// <param name="markupRate">The markup rate.</param>
		public static void SetMarkupRate(MarkupRate markupRate)
		{
			if (markupRate == null)
			{
				throw new ArgumentNullException("markupRate");
			}
			if (markupRate.Rate < 0)
			{
				throw new ArgumentException("Rate should not be smaller than zero.", "markupRate");
			}
			using (UnitFundsEntities dataContext = new UnitFundsEntities())
			{
				dataContext.MarkupRates.AddObject(markupRate);
				dataContext.SaveChanges();
			}
		}

		/// <summary>
		/// Sets the unit price.
		/// </summary>
		/// <param name="unitPrice">The unit price.</param>
		public static void SetUnitPrice(UnitPrice unitPrice)
		{
			if (unitPrice == null)
			{
				throw new ArgumentNullException("unitPrice");
			}
			if (unitPrice.Price < 0)
			{
				throw new ArgumentException("Price should not be smaller than zero.", "unitPrice");
			}
			using (UnitFundsEntities dataContext = new UnitFundsEntities())
			{
				dataContext.UnitPrices.AddObject(unitPrice);
				dataContext.SaveChanges();
			}
		}

		/// <summary>
		/// Writes off units amount for movement.
		/// </summary>
		/// <param name="unitsAmount">The units amount.</param>
		/// <param name="sourceUnitPrice">The source unit price.</param>
		/// <param name="destinationUnitPrice">The destination unit price.</param>
		/// <param name="incomesEnumerator">The incomes enumerator.</param>
		/// <param name="income">The income.</param>
		private static void WriteOffUnitsAmountForMovement(decimal unitsAmount, UnitPrice sourceUnitPrice, UnitPrice destinationUnitPrice, IEnumerator<Income> incomesEnumerator, Income income)
		{
			while (unitsAmount != 0 && incomesEnumerator.MoveNext())
			{
				Income sourceIncome = incomesEnumerator.Current;
				decimal incomeUnitPrice = sourceIncome.MoneyAmount / sourceIncome.UnitsAmount;
				decimal unitsToWriteOff = unitsAmount;
				decimal availableUnits = Math.Min(unitsToWriteOff, sourceIncome.UnitsAmount - (sourceIncome.UnitsExpenditureAmount ?? 0));
				sourceIncome.UnitsExpenditureAmount = (sourceIncome.UnitsExpenditureAmount ?? 0) + availableUnits;
				if (availableUnits != unitsToWriteOff)
				{
					unitsAmount -= availableUnits;
				}
				else
				{
					unitsAmount = 0;
				}
				income.MoneyAmount += availableUnits * incomeUnitPrice;
				income.UnitsAmount += availableUnits * sourceUnitPrice.Price / destinationUnitPrice.Price;
			}
			if (unitsAmount != 0)
			{
				throw new InvalidOperationException("There is no enough units to create movement.");
			}
		}

		/// <summary>
		/// Writes off money amount.
		/// </summary>
		/// <param name="expenditure">The expenditure.</param>
		/// <param name="expenditureDate">The expenditure date.</param>
		/// <param name="markdownRate">The markdown rate.</param>
		/// <param name="price">The price.</param>
		/// <param name="incomesEnumerator">The incomes enumerator.</param>
		private static void WriteOffMoneyAmount(
			Expenditure expenditure,
			DateTime expenditureDate,
			MarkdownRate markdownRate,
			UnitPrice price,
			IEnumerator<Income> incomesEnumerator)
		{
			decimal moneyToWriteOff = expenditure.MoneyAmount;
			while (moneyToWriteOff != 0 && incomesEnumerator.MoveNext())
			{
				Income income = incomesEnumerator.Current;
				decimal incomeUnitPrice = income.MoneyAmount / income.UnitsAmount;
				decimal expenditureUnitPrice = price.Price * (1 - markdownRate.Rate);
				decimal unitsToWriteOff;
				if (expenditureUnitPrice > incomeUnitPrice)
				{
					unitsToWriteOff = moneyToWriteOff / (price.Price + (TaxRate * (incomeUnitPrice - price.Price + (markdownRate.Rate * price.Price))) - (markdownRate.Rate * price.Price));
				}
				else
				{
					unitsToWriteOff = moneyToWriteOff / (price.Price - (markdownRate.Rate * price.Price));
				}
				decimal availableUnits = Math.Min(unitsToWriteOff, income.UnitsAmount - (income.UnitsExpenditureAmount ?? 0));
				income.UnitsExpenditureAmount = (income.UnitsExpenditureAmount ?? 0) + availableUnits;
				if (availableUnits != unitsToWriteOff)
				{
					moneyToWriteOff -= CalculatePossibleExpenditure(
						price.Price,
						availableUnits,
						markdownRate.Rate,
						incomeUnitPrice,
						income.Operation.OperationDate,
						expenditureDate,
						income.UnitFund.MarkdownPeriod);
				}
				else
				{
					moneyToWriteOff = 0;
				}
				expenditure.UnitsAmount += availableUnits;
			}
			if (moneyToWriteOff != 0)
			{
				throw new InvalidOperationException("There is no enough units to create expenditure.");
			}
		}

		/// <summary>
		/// Writes off units amount.
		/// </summary>
		/// <param name="expenditure">The expenditure.</param>
		/// <param name="expenditureDate">The expenditure date.</param>
		/// <param name="markdownRate">The markdown rate.</param>
		/// <param name="price">The price.</param>
		/// <param name="incomesEnumerator">The incomes enumerator.</param>
		private static void WriteOffUnitsAmount(
			Expenditure expenditure,
			DateTime expenditureDate,
			MarkdownRate markdownRate,
			UnitPrice price,
			IEnumerator<Income> incomesEnumerator)
		{
			decimal unitsToWriteOff = expenditure.UnitsAmount;
			while (unitsToWriteOff != 0 && incomesEnumerator.MoveNext())
			{
				Income income = incomesEnumerator.Current;
				decimal incomeUnitPrice = income.MoneyAmount / income.UnitsAmount;
				decimal availableUnits = Math.Min(unitsToWriteOff, income.UnitsAmount - (income.UnitsExpenditureAmount ?? 0));
				income.UnitsExpenditureAmount = (income.UnitsExpenditureAmount ?? 0) + availableUnits;
				unitsToWriteOff -= availableUnits;
				expenditure.MoneyAmount += CalculatePossibleExpenditure(
					price.Price,
					availableUnits,
					markdownRate.Rate,
					incomeUnitPrice,
					income.Operation.OperationDate,
					expenditureDate,
					income.UnitFund.MarkdownPeriod);
			}
			if (unitsToWriteOff != 0)
			{
				throw new InvalidOperationException("There is no enough units to create expenditure.");
			}
		}

		/// <summary>
		/// Calculates the possible expenditure.
		/// </summary>
		/// <param name="unitPrice">The unit price.</param>
		/// <param name="unitsCount">The units count.</param>
		/// <param name="markdownRate">The markdown rate.</param>
		/// <param name="incomeUnitPrice">The income unit price.</param>
		/// <param name="incomeDate">The income date.</param>
		/// <param name="calculationDate">The calculation date.</param>
		/// <param name="markdownPeriod">The markdown period.</param>
		/// <returns>Possible expenditure.</returns>
		private static decimal CalculatePossibleExpenditure(
			decimal unitPrice,
			decimal unitsCount,
			decimal markdownRate,
			decimal incomeUnitPrice,
			DateTime incomeDate,
			DateTime calculationDate,
			int markdownPeriod)
		{
			decimal purePossibleExpenditure = unitPrice * unitsCount;
			decimal markdown = 0;
			if (calculationDate.Subtract(incomeDate).TotalDays <= markdownPeriod)
			{
				markdown = purePossibleExpenditure * markdownRate;
			}
			decimal profit = purePossibleExpenditure - markdown - (incomeUnitPrice * unitsCount);
			decimal tax = 0;
			if (profit > 0)
			{
				tax = profit * TaxRate;
			}
			return purePossibleExpenditure - markdown - tax;
		}

		/// <summary>
		/// Adjusts the income units amount.
		/// </summary>
		/// <param name="income">The income.</param>
		/// <param name="markupRate">The markup rate.</param>
		/// <param name="price">The price.</param>
		private static void AdjustIncomeUnitsAmount(Income income, MarkupRate markupRate, UnitPrice price)
		{
			if (income.UnitsAmount == 0)
			{
				decimal pureMoneyToBuy = income.MoneyAmount / (1 + markupRate.Rate);
				income.UnitsAmount = pureMoneyToBuy / price.Price;
			}
			else if (income.MoneyAmount == 0)
			{
				income.MoneyAmount = (income.UnitsAmount * price.Price) * (1 + markupRate.Rate);
			}
		}

		/// <summary>
		/// Gets the active incomes.
		/// </summary>
		/// <param name="finalDate">The final date.</param>
		/// <param name="dataContext">The data context.</param>
		/// <param name="unitFundId">The unit fund ID.</param>
		/// <returns>Incomes which is not all expended yet.</returns>
		private static IEnumerable<Income> GetActiveIncomes(DateTime finalDate, UnitFundsEntities dataContext, Guid unitFundId)
		{
			return
				from income in dataContext.Incomes
				join operation in dataContext.Operations
					on income.IncomeId equals operation.OperationId
				where (income.UnitsAmount != income.UnitsExpenditureAmount || income.UnitsExpenditureAmount == null) && income.FundId == unitFundId && operation.OperationDate <= finalDate
				orderby operation.OperationDate ascending
				select income;
		}

		/// <summary>
		/// Gets the last markdown rate.
		/// </summary>
		/// <param name="finalDate">The final date.</param>
		/// <param name="dataContext">The data context.</param>
		/// <param name="unitFundId">The unit fund ID.</param>
		/// <returns>Markdown rate.</returns>
		private static MarkdownRate GetLastMarkdownRate(DateTime finalDate, UnitFundsEntities dataContext, Guid unitFundId)
		{
			MarkdownRate result =
				(from item in dataContext.MarkdownRates
				 where item.FundId == unitFundId && item.Date < finalDate
				 orderby item.Date descending
				 select item).Take(1).Single();
			return result;
		}

		/// <summary>
		/// Gets the last markup rate.
		/// </summary>
		/// <param name="finalDate">The final date.</param>
		/// <param name="dataContext">The data context.</param>
		/// <param name="unitFundId">The unit fund ID.</param>
		/// <returns>Markup rate.</returns>
		private static MarkupRate GetLastMarkupRate(DateTime finalDate, UnitFundsEntities dataContext, Guid unitFundId)
		{
			MarkupRate markupRate =
				(from item in dataContext.MarkupRates
				 where item.FundId == unitFundId && item.Date < finalDate
				 orderby item.Date descending
				 select item).Take(1).Single();
			return markupRate;
		}

		/// <summary>
		/// Gets the first price.
		/// </summary>
		/// <param name="startDate">The start date.</param>
		/// <param name="dataContext">The data context.</param>
		/// <param name="unitFundId">The unit fund ID.</param>
		/// <returns>Unit price.</returns>
		private static UnitPrice GetFirstPrice(DateTime startDate, UnitFundsEntities dataContext, Guid unitFundId)
		{
			UnitPrice price =
				(from item in dataContext.UnitPrices
				 where item.FundId == unitFundId && item.PriceDate >= startDate
				 orderby item.PriceDate ascending
				 select item).Take(1).SingleOrDefault();
			return price;
		}

		/// <summary>
		/// Calculates the incomes.
		/// </summary>
		/// <param name="dataContext">The data context.</param>
		/// <param name="calculatingHelper">The calculating helper.</param>
		private static void CalculateIncomes(UnitFundsEntities dataContext, ProfitabilityCalculatingHelper calculatingHelper)
		{
			var incomes =
				from income in dataContext.Incomes
				join operation in dataContext.Operations
					on income.IncomeId equals operation.OperationId
				join tempExpenditure in dataContext.Expenditures
					on income.IncomeId equals tempExpenditure.ExpenditureId
					into expenditures
				from expenditure in expenditures.DefaultIfEmpty()
				where !operation.IsPendingOperation && operation.OperationDate <= calculatingHelper.EndDate
				select new
				{
					OperationType = (OperationType)operation.OperationTypeId,
					Date = operation.OperationDate,
					IncomeData = income,
					ExpenditureData = expenditure
				};
			foreach (var income in incomes)
			{
				Guid unitFundId = income.IncomeData.FundId;
				UnitFund unitFund = income.IncomeData.UnitFund;
				FundProfitability fundProfitability;
				if (!calculatingHelper.FundsProfitabilites.ContainsKey(unitFundId))
				{
					fundProfitability = CreateFundProfitability(calculatingHelper, unitFund);
					calculatingHelper.FundsProfitabilites.Add(unitFundId, fundProfitability);
				}
				else
				{
					fundProfitability = calculatingHelper.FundsProfitabilites[unitFundId];
				}
				if (income.Date > calculatingHelper.StartDate)
				{
					switch (income.OperationType)
					{
						case OperationType.Income:
							fundProfitability.OperationsAmount += income.IncomeData.MoneyAmount;
							break;
						case OperationType.Exchange:
							fundProfitability.OperationsAmount += income.ExpenditureData.MoneyAmount;
							break;
						default:
							throw new InvalidOperationException("Invalid operation type for income.");
					}
				}
				if (income.Date <= calculatingHelper.StartDate)
				{
					calculatingHelper.StartFundsSnapshots[unitFundId].UpdateWithIncomeOperation(income.IncomeData);
				}
				calculatingHelper.EndFundsSnapshots[unitFundId].UpdateWithIncomeOperation(income.IncomeData);
			}
		}

		/// <summary>
		/// Creates the fund profitability.
		/// </summary>
		/// <param name="calculatingHelper">The calculating helper.</param>
		/// <param name="unitFund">The unit fund.</param>
		/// <returns>Fund profitability with initial values.</returns>
		private static FundProfitability CreateFundProfitability(ProfitabilityCalculatingHelper calculatingHelper, UnitFund unitFund)
		{
			FundProfitability fundProfitability;
			fundProfitability = new FundProfitability();
			fundProfitability.EndDate = calculatingHelper.EndDate;
			fundProfitability.FundId = unitFund.FundId;
			fundProfitability.FundName = unitFund.FundName;
			fundProfitability.StartDate = calculatingHelper.StartDate;
			return fundProfitability;
		}

		/// <summary>
		/// Calculates the expenditures.
		/// </summary>
		/// <param name="dataContext">The data context.</param>
		/// <param name="calculatingHelper">The calculating helper.</param>
		private static void CalculateExpenditures(UnitFundsEntities dataContext, ProfitabilityCalculatingHelper calculatingHelper)
		{
			var expenditures =
				from expenditure in dataContext.Expenditures
				join operation in dataContext.Operations
					on expenditure.ExpenditureId equals operation.OperationId
				where operation.OperationDate <= calculatingHelper.EndDate
				select new
				{
					Date = operation.OperationDate,
					OperationData = expenditure
				};
			foreach (var expenditure in expenditures)
			{
				Guid unitFundId = expenditure.OperationData.FundId;
				UnitFund unitFund = expenditure.OperationData.UnitFund;
				FundProfitability fundProfitability;
				if (!calculatingHelper.FundsProfitabilites.ContainsKey(unitFundId))
				{
					fundProfitability = CreateFundProfitability(calculatingHelper, unitFund);
					calculatingHelper.FundsProfitabilites.Add(unitFundId, fundProfitability);
				}
				else
				{
					fundProfitability = calculatingHelper.FundsProfitabilites[unitFundId];
				}
				if (expenditure.Date > calculatingHelper.StartDate)
				{
					fundProfitability.OperationsAmount -= expenditure.OperationData.MoneyAmount;
				}
				if (expenditure.Date <= calculatingHelper.StartDate)
				{
					calculatingHelper.StartFundsSnapshots[unitFundId].UpdateWithExpenditureOperation(expenditure.OperationData);
				}
				calculatingHelper.EndFundsSnapshots[unitFundId].UpdateWithExpenditureOperation(expenditure.OperationData);
			}
		}

		/// <summary>
		/// Gets the unit prices.
		/// </summary>
		/// <param name="finalDate">The final date.</param>
		/// <param name="dataContext">The data context.</param>
		/// <returns>Unit prices grouped by unit fund ID.</returns>
		private static IQueryable<IGrouping<Guid, UnitPrice>> GetUnitPrices(DateTime finalDate, UnitFundsEntities dataContext)
		{
			return
				from unitPrice in dataContext.UnitPrices
				where unitPrice.PriceDate <= finalDate
				group unitPrice by unitPrice.FundId;
		}

		/// <summary>
		/// Creates the fund snapshots.
		/// </summary>
		/// <param name="date">The date of the snapshot.</param>
		/// <param name="unitFunds">The unit funds.</param>
		/// <param name="unitPricesGroups">The unit prices groups.</param>
		/// <param name="markdownRatesGroups">The markdown rates groups.</param>
		/// <returns>Fund snapshots grouped by unit fund ID.</returns>
		private static Dictionary<Guid, FundSnapshot> CreateFundSnapshots(
			DateTime date,
			IEnumerable<UnitFund> unitFunds,
			IQueryable<IGrouping<Guid, UnitPrice>> unitPricesGroups,
			IQueryable<IGrouping<Guid, MarkdownRate>> markdownRatesGroups)
		{
			Dictionary<Guid, FundSnapshot> result = new Dictionary<Guid, FundSnapshot>();
			foreach (UnitFund unitFund in unitFunds)
			{
				FundSnapshot snapshot = new FundSnapshot();
				snapshot.UnitFund = unitFund;
				snapshot.Date = date;
				snapshot.UnitPrice = 0;
				snapshot.MarkdownRate = 0;
				result.Add(unitFund.FundId, snapshot);
			}
			foreach (IGrouping<Guid, UnitPrice> unitPrices in unitPricesGroups)
			{
				FundSnapshot snapshot = null;
				if (!result.ContainsKey(unitPrices.Key))
				{
					snapshot = new FundSnapshot();
					result.Add(unitPrices.Key, snapshot);
				}
				else
				{
					snapshot = result[unitPrices.Key];
				}
				snapshot.UnitPrice = unitPrices.OrderBy((unitPrice) => unitPrice.PriceDate).Last().Price;
			}
			foreach (IGrouping<Guid, MarkdownRate> markdownRates in markdownRatesGroups)
			{
				FundSnapshot snapshot = null;
				if (!result.ContainsKey(markdownRates.Key))
				{
					snapshot = new FundSnapshot();
					result.Add(markdownRates.Key, snapshot);
				}
				else
				{
					snapshot = result[markdownRates.Key];
				}
				snapshot.MarkdownRate = markdownRates.OrderBy((markdownRate) => markdownRate.Date).Last().Rate;
			}
			return result;
		}

		/// <summary>
		/// Gets the markdown rates.
		/// </summary>
		/// <param name="finalDate">The final date.</param>
		/// <param name="dataContext">The data context.</param>
		/// <returns>
		/// Groups of markdown rates by unit fund ID.
		/// </returns>
		private static IQueryable<IGrouping<Guid, MarkdownRate>> GetMarkdownRates(DateTime finalDate, UnitFundsEntities dataContext)
		{
			var markdownRates =
				from markdownRate in dataContext.MarkdownRates
				where markdownRate.Date <= finalDate
				group markdownRate by markdownRate.FundId;
			return markdownRates;
		}
		#endregion

		#region Helper types
		/// <summary>
		/// Class which helps to calculate unit fund profitability.
		/// </summary>
		private class ProfitabilityCalculatingHelper
		{
			private DateTime _startDate;
			private DateTime _endDate;
			private Dictionary<Guid, FundProfitability> _fundsProfitabilities;
			private Dictionary<Guid, FundSnapshot> _startFundsSnapshots;
			private Dictionary<Guid, FundSnapshot> _endFundsSnapshots;

			/// <summary>
			/// Gets or sets the start date.
			/// </summary>
			/// <value>The start date.</value>
			internal DateTime StartDate
			{
				get
				{
					return _startDate;
				}

				set
				{
					_startDate = value;
				}
			}

			/// <summary>
			/// Gets or sets the end date.
			/// </summary>
			/// <value>The end date.</value>
			internal DateTime EndDate
			{
				get
				{
					return _endDate;
				}

				set
				{
					_endDate = value;
				}
			}

			/// <summary>
			/// Gets or sets the funds profitabilites.
			/// </summary>
			/// <value>The funds profitabilites.</value>
			internal Dictionary<Guid, FundProfitability> FundsProfitabilites
			{
				get
				{
					return _fundsProfitabilities;
				}

				set
				{
					_fundsProfitabilities = value;
				}
			}

			/// <summary>
			/// Gets or sets the start funds snapshots.
			/// </summary>
			/// <value>The start funds snapshots.</value>
			internal Dictionary<Guid, FundSnapshot> StartFundsSnapshots
			{
				get
				{
					return _startFundsSnapshots;
				}

				set
				{
					_startFundsSnapshots = value;
				}
			}

			/// <summary>
			/// Gets or sets the end funds snapshots.
			/// </summary>
			/// <value>The end funds snapshots.</value>
			internal Dictionary<Guid, FundSnapshot> EndFundsSnapshots
			{
				get
				{
					return _endFundsSnapshots;
				}

				set
				{
					_endFundsSnapshots = value;
				}
			}
		}

		/// <summary>
		/// Snapshot of unit fund for given point in time.
		/// </summary>
		private class FundSnapshot
		{
			private List<Income> _incomes;
			private decimal _unitPrice;
			private decimal _markdownRate;
			private DateTime _date;
			private UnitFund _unitFund;

			/// <summary>
			/// Initializes a new instance of the <see cref="FundSnapshot"/> class.
			/// </summary>
			internal FundSnapshot()
			{
				_incomes = new List<Income>();
			}

			/// <summary>
			/// Gets or sets the date of snapshot.
			/// </summary>
			/// <value>The date of snapshot.</value>
			internal DateTime Date
			{
				get
				{
					return _date;
				}

				set
				{
					_date = value;
				}
			}

			/// <summary>
			/// Gets or sets the unit fund.
			/// </summary>
			/// <value>The unit fund.</value>
			internal UnitFund UnitFund
			{
				get
				{
					return _unitFund;
				}

				set
				{
					_unitFund = value;
				}
			}

			/// <summary>
			/// Gets or sets the unit price.
			/// </summary>
			/// <value>The unit price.</value>
			internal decimal UnitPrice
			{
				get
				{
					return _unitPrice;
				}

				set
				{
					_unitPrice = value;
				}
			}

			/// <summary>
			/// Gets or sets the markdown rate.
			/// </summary>
			/// <value>The markdown rate.</value>
			internal decimal MarkdownRate
			{
				get
				{
					return _markdownRate;
				}

				set
				{
					_markdownRate = value;
				}
			}

			/// <summary>
			/// Gets the possible money expenditure.
			/// </summary>
			/// <value>The possible money expenditure.</value>
			internal decimal PossibleMoneyExpenditure
			{
				get
				{
					decimal result = 0;
					foreach (Income income in _incomes)
					{
						decimal unitsCount = income.UnitsAmount - (income.UnitsExpenditureAmount ?? 0);
						decimal moneyIncome = income.MoneyAmount - (income.MoneyAmount * ((income.UnitsExpenditureAmount ?? 0) / income.UnitsAmount));
						result += CalculatePossibleExpenditure(
							UnitPrice,
							unitsCount,
							MarkdownRate,
							moneyIncome / unitsCount,
							income.Operation.OperationDate,
							Date,
							UnitFund.MarkdownPeriod);
					}
					return result;
				}
			}

			/// <summary>
			/// Updates the with income operation.
			/// </summary>
			/// <param name="income">The income.</param>
			internal void UpdateWithIncomeOperation(Income income)
			{
				if (income == null)
				{
					throw new ArgumentNullException("income");
				}
				Income initialIncome = new Income();
				initialIncome.MoneyAmount = income.MoneyAmount;
				initialIncome.UnitsAmount = income.UnitsAmount;
				initialIncome.UnitsExpenditureAmount = 0;
				Operation operation = new Operation();
				operation.OperationDate = income.Operation.OperationDate;
				initialIncome.Operation = operation;
				_incomes.Add(initialIncome);
			}

			/// <summary>
			/// Updates the with expenditure operation.
			/// </summary>
			/// <param name="expenditure">The expenditure.</param>
			internal void UpdateWithExpenditureOperation(Expenditure expenditure)
			{
				if (expenditure == null)
				{
					throw new ArgumentNullException("expenditure");
				}
				decimal unitsAmountToWriteOff = expenditure.UnitsAmount;
				while (unitsAmountToWriteOff != 0)
				{
					Income incomeToWriteOff = _incomes[0];
					decimal incomeUnitsRemainder = incomeToWriteOff.UnitsAmount - (incomeToWriteOff.UnitsExpenditureAmount ?? 0);
					if (unitsAmountToWriteOff >= incomeUnitsRemainder)
					{
						unitsAmountToWriteOff -= incomeUnitsRemainder;
						_incomes.RemoveAt(0);
					}
					else
					{
						incomeToWriteOff.UnitsExpenditureAmount += unitsAmountToWriteOff;
						unitsAmountToWriteOff = 0;
					}
				}
			}
		}
		#endregion
	}
}
